#include <cnix/string.h>
#include <cnix/errno.h>
#include <cnix/fs.h>
#include <cnix/kernel.h>
#include <cnix/mm.h>

/* 
 * return value:
 *   1 ok
 *   0 fail
 */
int admit(struct inode * inoptr, mode_t expect)
{
	mode_t mode;
	uid_t euid;
	gid_t egid;

	if(is_socket(inoptr) || is_pipe(inoptr)){
		PRINTK("BUG: cannot happen\n");
		return 1;
	}

	mode = inoptr->i_ops->mode(inoptr);

	euid = current->euid;
	egid = current->egid;

	// super user
	if(euid == SU_UID){
		// expect executable, but not a executable file
		if((expect & 1) && !(mode & (1 + 8 + 64)))
			return 0;

		return 1;
	}

	// owner
	if((euid == inoptr->i_ops->getuid(inoptr)) && ((expect << 6) & mode))
		return 1;

	// same group
	if((egid == inoptr->i_ops->getgid(inoptr)) && ((expect << 3) & mode))
		return 1;

	// other user
	if(expect & mode)
		return 1;

	return 0;
}

/*
 * return value:
 *   NULL: end of name
 *     -1: name is too long
 *    oth: next char
 */
static char * get_next_name(char * nextch, char * part, int flag)
{
	int length;
	char * prevch;

	if(!nextch[0])
		return NULL;

	prevch = nextch;
	nextch = strchr(nextch, '/');

	if(nextch){
		length = nextch - prevch;

		while(nextch[0] == '/')
			nextch++;
	}else{
		length = strlen(prevch);
		nextch = &prevch[length];
	}

	if(!length)
		return NULL;

	if(length > NAME_MAX)
		return (char *)-1;

	if(flag && !nextch[0])
		return NULL;

	memcpy(part, prevch, length);
	part[length] = '\0';

	return nextch;
}

#define MAX_SYMLINK_DEPTH 32

/* 
 * flag:
 *   0 find filename's inode
 *   1 find filename's father's inode
 * return a locked inode
 */
static struct inode * __namei(
	const char * filename, int * error, int flag, int into_link,
	struct inode * inoptr
	)
{
	dev_t prevdev;
	ino_t previno;
	int level, end_with_slash;
	struct inode * workino, * foundino;
	char * nextch, * prevch, part[NAME_MAX + 1], * page, * page1;

	level = 0;
	end_with_slash = 0;
	prevdev = current->pwd->i_dev;
	previno = current->pwd->i_num;

	page = (char *)get_one_page();
	if(!page){
		*error = -ENOMEM;
		return NULL;
	}

	page1 = (char *)get_one_page();
	if(!page1){
		free_one_page((unsigned long)page);
		*error = -ENOMEM;
		return NULL;
	}

	nextch = (char *)filename;

step_into_link:

	if(!nextch[0])
		goto errout1;	

	if(nextch[strlen(nextch) - 1] == '/')
		end_with_slash = 1;
	
	if(nextch[0] == '/'){
		// skip '/' before calling get_next_name
		while(nextch[0] == '/')
			nextch++;

		prevdev = current->root->i_dev;
		previno = current->root->i_num;
	}

	workino = iget(prevdev, previno);

	*error = 0;

	do{
		// bypass
		if(inoptr && (workino == inoptr))
			break;

		prevch = nextch;
		nextch = get_next_name(nextch, part, flag);

		if(S_ISLNK(workino->i_ops->mode(workino))){
			if(!nextch && !into_link)
				break;

			if(level > MAX_SYMLINK_DEPTH){
				*error = -EMLINK;
				goto errout;
			}

			level++;

			if(!workino->i_ops->real_filename(
				page1, workino, error))
				goto errout;

			if(nextch){
				// just for simple
				if(PAGE_SIZE - strlen(page1)
					< strlen(prevch) + 1 + 1){
					*error = -ENAMETOOLONG;
					goto errout;
				}

				strcat(page1, "/");
				strcat(page1, prevch);
			}

			// switch page and page1
			{
				char * tmp;

				tmp = page;
				page = page1;
				page1 = tmp;
			}

			iput(workino);

			nextch = page;
			goto step_into_link;
		}

		/*
                 * "xxx/", "xxxxxxx/////"
		 *     ^           ^^^^^ 
	 	 */
		if(!nextch){
			// must be a directory
			if(end_with_slash
				&& !S_ISDIR(workino->i_ops->mode(workino))){
				*error = -ENOTDIR;
				goto errout;
			}

			break;
		}

		if(nextch == (char *)-1){
			*error = -ENAMETOOLONG;
			goto errout;
		}

		// xxx, readble or excutable?
		if(!admit(workino, I_XB)){
			*error = -EACCES;
			goto errout;
		}

		prevdev = workino->i_dev;
		previno = workino->i_num;

		// ifind will iput workino
		foundino = ifind(workino, part);
		if(!foundino){
			*error = -ENOENT;
			goto errout1;
		}

		workino = foundino;
	}while(1);

	free_one_page((unsigned long)page);
	free_one_page((unsigned long)page1);

	return workino;

errout:
	iput(workino);
errout1:
	free_one_page((unsigned long)page);
	free_one_page((unsigned long)page1);

	return NULL;
}

/* 
 * flag:
 *   0 find filename's inode
 *   1 find filename's father's inode
 * return a locked inode
 */
struct inode * namei(const char * filename, int * error, int flag)
{
	return __namei(filename, error, flag, 1, NULL);
}

/* 
 * flag:
 *   0 find filename's inode
 *   1 find filename's father's inode
 * return a locked inode
 */
struct inode * namei0(const char * filename, int * error, int flag)
{
	return __namei(filename, error, flag, 0, NULL);
}

/* 
 * flag:
 *   0 check filename
 *   1 check filename's father
 * inoptr:
 *   a unlocked inode
 * return:
 *   0: not bypass
 *   1: bypass
 */
int bypass(
	const char * filename, int * error, int flag, int into_link,
	struct inode * inoptr
	)
{
	int ret = 0;
	struct inode * inoptr1;

	inoptr1 = __namei(filename, error, flag, 1, inoptr);
	if(inoptr1){
		if(inoptr == inoptr1)
			ret = 1;

		iput(inoptr1);
	}

	return ret;
}

int get_tail(const char *pathname, char *part)
{
	const char *ptr;
	const char *p;
	int len;

	if (!(len = strlen(pathname)))
	{
		return 0;
	}

	// get the first char which is not '/' from the end
	p = &pathname[len - 1];
	while (*p == '/' && p > pathname)
	{
		p--;
	}

	if (*p == '/')	// all are '/'
	{
		return 0;
	}

	// get the second '/' from the end
	ptr = p;
	while (*ptr != '/' && ptr > pathname)
	{
		ptr--;
	}

	if (*ptr == '/')
	{
		ptr++;
	}

	if ((len = p - ptr + 1) > NAME_MAX)
	{
		return 0;
	}

	memcpy(part, ptr, len);
	part[len] = '\0';

	return 1;
}

/*
 * return a locked inode,
 * inoptr is a locked inode, and will be iput here always
 */
struct inode * ifind(struct inode * inoptr, const char * name)
{
	ino_t ino;
	struct super_block * sb;
	struct inode * workino;

	if(!S_ISDIR(inoptr->i_ops->mode(inoptr))){
		iput(inoptr);
		return NULL;
	}

	if(!strcmp("..", name)){
		if(inoptr->i_flags & MOUNTED_I){
			if(REAL_ROOT(inoptr))
				return inoptr;

			sb = inoptr->i_sb;
			if(!sb)
				DIE("BUG: cannot happen\n");

			workino = sb->s_mounti;

			workino->i_count++;
			ilock(workino);

			iput(inoptr);
		}else if(inoptr == current->root){
			return inoptr;
		}else
			workino = inoptr;
	}else
		workino = inoptr;

	ino = workino->i_ops->findson(workino, name);
	if(ino != NOINO){
		struct inode * tmp;
		ino_t fnum = workino->i_num;

		// won't be blocked, so can use it below immediately
		iput(workino);

		tmp = iget(workino->i_dev, ino);
		if(tmp->i_fnum == NOINO)
			tmp->i_fnum = fnum;

		return tmp;
	}

	iput(workino);

	return NULL;
}

/*
 * return a locked inode,
 * inoptr is a locked inode, and will remain in locked state
 */
struct inode * iinsert(
	struct inode * inoptr, const char * name, mode_t mode, int * error
	)
{
	struct inode * tmp;

	if(!S_ISDIR(inoptr->i_ops->mode(inoptr)))
		DIE("BUG: cannot happen\n");

	// this flag must be checked outside
	if(inoptr->i_sb->s_roflag){
		*error = -EROFS;
		return NULL;
	}

	tmp = ialloc(inoptr->i_dev, mode, error);
	if(!tmp){
		PRINTK("out of disk space\n");
		*error = -ENOSPC;
		return NULL;
	}

	if(!inoptr->i_ops->additem(inoptr, name, error, tmp->i_num)){
		tmp->i_ops->change_nlinks(tmp, -1);
		iput(tmp);
		return NULL;
	}

	tmp->i_fnum = inoptr->i_num;

	return tmp;
}
